﻿// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.ComponentModel.Design.Serialization;

namespace System.ComponentModel.Design;

public abstract partial class UndoEngine
{
    protected partial class UndoUnit
    {
        private sealed class ChangeUndoEvent : UndoEvent
        {
            // Static data we hang onto about this change.
            private readonly string _componentName;
            private readonly MemberDescriptor? _member;
            // Before and after state. Before state is built in the constructor.
            // After state is built right before we undo for the first time.
            private SerializationStore? _before;
            private SerializationStore? _after;
            private bool _savedAfterState;

            /// <summary>
            ///  Creates a new component change undo event.
            ///  This event consists of a before and after snapshot of a single component.
            ///  A snapshot will not be taken if a name for the component cannot be determined.
            /// </summary>
            public ChangeUndoEvent(UndoEngine engine, ComponentChangingEventArgs e, bool serializeBeforeState)
            {
                _componentName = engine.GetName(e.Component, true)!;
                OpenComponent = e.Component;
                _member = e.Member;

                if (serializeBeforeState)
                {
                    _before = Serialize(engine, OpenComponent!, _member);
                }
            }

            public ComponentChangingEventArgs ComponentChangingEventArgs => new(OpenComponent, _member);

            /// <summary>
            ///  Indicates that undoing this event may cause side effects in other objects.
            ///  Change events fall into this category because, for example, a change involving adding an object
            ///  to one collection may have a side effect of removing it from another collection.
            ///  Events with side effects are grouped at undo time so all their BeforeUndo methods
            ///  are called before their Undo methods.
            ///  Events without side effects have their BeforeUndo called and then their Undo called immediately after.
            /// </summary>
            public override bool CausesSideEffects => true;

            /// <summary>
            ///  Returns true if the change event has been comitted.
            /// </summary>
            [MemberNotNullWhen(false, nameof(OpenComponent))]
            public bool Committed => OpenComponent is null;

            /// <summary>
            ///  Returns the component this change event is currently tracking.
            ///  This will return null once the change event is committed.
            /// </summary>
            public object? OpenComponent { get; private set; }

            /// <summary>
            ///  Called before Undo is called. All undo events get their BeforeUndo called,
            ///  and then they all get their Undo called. This allows the undo event to examine
            ///  the state of the world before other undo events mess with it.
            /// </summary>
            public override void BeforeUndo(UndoEngine engine)
            {
                if (!_savedAfterState)
                {
                    _savedAfterState = true;
                    SaveAfterState(engine);
                }
            }

            /// <summary>
            ///  Determines if this
            /// </summary>
            public bool ContainsChange(MemberDescriptor? desc)
            {
                if (_member is null)
                {
                    return true;
                }

                if (desc is null)
                {
                    return false;
                }

                return desc.Equals(_member);
            }

            /// <summary>
            ///  Commits the unit. Committing the unit saves the "after" snapshot of the unit.
            ///  If commit is called multiple times only the first commit is registered.
            /// </summary>
            public void Commit()
            {
                if (!Committed)
                {
                    OpenComponent = null;
                }
            }

            private void SaveAfterState(UndoEngine engine)
            {
                Debug.Assert(_after is null, "Change undo saving state twice.");
                object? component = null;

                if (engine.TryGetService(out IReferenceService? rs))
                {
                    component = rs.GetReference(_componentName);
                }
                else if (engine.TryGetService(out IDesignerHost? host))
                {
                    component = host.Container.Components[_componentName];
                }

                // It is OK for us to not find a component here.
                // That can happen if our "after" state is owned by another change,
                // like an add of the component.
                if (component is not null)
                {
                    _after = Serialize(engine, component, _member);
                }
            }

            private static SerializationStore Serialize(UndoEngine engine, object component, MemberDescriptor? member)
            {
                SerializationStore store;
                using (store = engine._serializationService.CreateStore())
                {
                    if (member is not null && !(member.Attributes.Contains(DesignerSerializationVisibilityAttribute.Hidden)))
                    {
                        engine._serializationService.SerializeMemberAbsolute(store, component, member);
                    }
                    else
                    {
                        engine._serializationService.SerializeAbsolute(store, component);
                    }
                }

                return store;
            }

            /// <summary>
            ///  Performs the actual undo. After it finishes it will reverse the role of _before and _after
            /// </summary>
            public override void Undo(UndoEngine engine)
            {
                Debug.Assert(_savedAfterState, "After state not saved. BeforeUndo was not called?");

                if (_before is not null)
                {
                    if (engine.TryGetService(out IDesignerHost? host))
                    {
                        engine._serializationService.DeserializeTo(_before, host.Container);
                    }
                }

                (_after, _before) = (_before, _after);
            }
        }
    }
}
